package myJs.cps

import cats.effect._
import japgolly.scalajs.react._
import japgolly.scalajs.react.util.EffectCatsEffect._
import japgolly.scalajs.react.vdom.TagOf
import japgolly.scalajs.react.vdom.all._
import myJs.Implicits._
import myJs.Utils._
import myJs.cps.myTable._
import myJs.myPkg.Hooks._
import myJs.myPkg.i18n.ReactI18next
import myJs.myPkg.jquery.$
import myJs.myPkg.reactHookForm._
import myJs.tool.Pojo._
import myJs.tool.Tool
import org.scalajs.dom.html.Span
import org.scalajs.dom.{Element, Event, document}
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._
import upickle.default._

import scala.collection.immutable.SeqMap
import scala.concurrent.Future
import scala.scalajs.js
import scala.scalajs.js.timers.SetTimeoutHandle

/** Created by yz on 15/9/2023
  */
object BootstrapTableCp {

  case class Column(
      field: String,
      title: String = "",
      operable: Boolean = false,
      formatter: js.UndefOr[(ujson.Value, ujson.Obj) => VdomNode] = js.undefined,
      searchKind: SearchKind = TextSearch,
      comVF: (ujson.Value, ujson.Obj) => String,
      optionMap: SeqMap[String, String] = SeqMap.empty,
      defaultValue: String = "",
      checkbox: Boolean = false,
      titleE: js.UndefOr[TagOf[Span]] = js.undefined
  )

  case class Props(
      columns: List[Column],
      datas: List[ujson.Obj],
      pagination: Boolean = true,
      pageList: List[String] = List("10", "25", "50", "100"),
      pageSize: String = "10",
      sidePagination: String = "client",
      ajax: js.UndefOr[(Int, Int, SeqMap[String, SearchData], Option[SortData], String) => Future[ujson.Obj]] =
        js.undefined,
      selectColumnEnabled: Boolean = false,
      idField: String = "id",
      selectedIds: List[String] = List.empty,
      selectedIdsChange: List[String] => SyncIO[Unit] = x => SyncIO {},
      showExport: Boolean = false,
      exportData: List[ExportData] = List.empty,
      search: Boolean = false,
      showColumnNum: Int = 8,
      showColumnNames: List[String] = List.empty,
      allSearchTitles: List[String] = List.empty,
      columnGroupMap: SeqMap[String, List[String]] = SeqMap.empty,
      extraFixedColumns: List[String] = List.empty,
      allSearch: String = ""
  ) {
    @inline def render: VdomElement = {
      Component(this)
    }
  }

  val Component = ScalaFnComponent[Props] { props =>
    val (sortField, setSortField) = useState("")
    val (order, setOrder) = useState("")
    val (searchMap, setSearchMap) = useState(SeqMap[String, SearchData]())
    val (tmpSearchMap, setTmpSearchMap) = useState(SeqMap[String, SearchData]())
    val (pageNumber, setPageNumber) = useState(1)
    val (pageSize, setPageSize) = useState(props.pageSize)
    val (menuField, setMenuField) = useState("")
    val (menuShow, setMenuShow) = useState(false)
    val (serverDatas, setServerDatas) = useState(List.empty[ujson.Obj])
    val (serverTotalRows, setServerTotalRows) = useState(0)
    val (columnShowMap, setColumnShowMap) = useState(SeqMap[String, Boolean]())
    val (hasFillDefaultV, setHasFillDefaultV) = useState(false)
    val (allSearch, setAllSearch) = useState("")

    val t = ReactI18next.useT
    val isServer = props.sidePagination == "server"

    val fieldColumnMap = props.columns.map { x =>
      (x.field, x)
    }.toSeqMap

    val useForm = ReactHookForm.useForm(UseFormOptions(mode = "onChange"))

    object FTool {

      def refreshServerDatas = {
        if (isServer && props.columns.nonEmpty) {
          props.ajax.map { ajax =>
            val pageSizeNum = if (pageSize.isDouble) pageSize.toDouble.toInt else Int.MaxValue
            val offset = pageSizeNum * (pageNumber - 1)
            val limit = pageSizeNum
            val sortData = if (sortField.nonBlank) {
              val column = fieldColumnMap(sortField)
              val kind = column.searchKind match {
                case NumericSearch => "int"
                case DoubleSearch => "double"
                case _ => "str"
              }
              Some(SortData(field = sortField, kind = kind, order = order))
            } else None
            ajax(limit, offset, searchMap, sortData, allSearch).map { rs =>
              val tableRs = rs("rows").transform(reader[List[ujson.Obj]])
              val total = rs("total").num.toInt
              setServerDatas(tableRs)
              setServerTotalRows(total)
            }
          }
        }
      }

      def tmpSearchMapChange = (f: SeqMap[String, SearchData] => SeqMap[String, SearchData]) => {
        setTmpSearchMap { x =>
          f(x)
        }
      }

      def removeSort = {
        setSortField("") >>
          setOrder("")
      }

      val pageSizeChange = (x: String) => {
        setPageSize(x)
      }

      val pageNumberChange = (x: Int) => {
        setPageNumber(x)
      }

      def confirmFilter(curField: String) = {
        val curSearchMap = FTool.tmp2True(tmpSearchMap)
        setSearchMap(curSearchMap) >>
          setMenuShow(false)
      }

      val columnShowMapChange = (x: SeqMap[String, Boolean]) => {
        setColumnShowMap(x)
      }

      def tmp2True(tmpSearchMap: SeqMap[String, SearchData]) = {
        tmpSearchMap.filter { case (field, searchData) =>
          searchData match {
            case DateSearchData(startDate, endDate) => startDate.nonBlank || endDate.nonBlank
            case TextSearchData(searchType, text) => text.nonBlank
            case NumericSearchData(min, max, kind) => min.nonBlank || max.nonBlank
            case CheckboxSearchData(values) => values.nonEmpty
            case OnlySortSearchData => true
          }
        }
      }

      def removeCondition = (curField: String) => {
        val curTmpSearchMap = tmpSearchMap.updatedWith(curField) { op =>
          op.map { x =>
            x match {
              case y @ DateSearchData(startDate, endDate) => y.copy(startDate = "", endDate = "")
              case y @ TextSearchData(searchType, text) => y.copy(text = "")
              case y @ NumericSearchData(min, max, kind) => y.copy(min = "", max = "")
              case y @ CheckboxSearchData(values) => y.copy(values = List.empty[String])
            }
          }
        }
        val curSearchMap = FTool.tmp2True(curTmpSearchMap)
        setTmpSearchMap(curTmpSearchMap) >>
          setSearchMap(curSearchMap)
      }

      def sortExec = {
        val initDatas = props.datas
        val (ascSortData, blankDatas) = if (sortField.isBlank) {
          (initDatas, List())
        } else {
          val column = fieldColumnMap(sortField)
          column.searchKind match {
            case NumericSearch =>
              val blankDatas = initDatas.filter(_(sortField).myStr.isBlank)
              val notBlankDatas = initDatas.filterNot(_(sortField).myStr.isBlank).sortBy { x =>
                column.comVF(x(sortField), x).toDouble
              }
              (notBlankDatas, blankDatas)
            case _ =>
              val sortData = initDatas.sortBy(_.obj.getOrElse(sortField, ujson.Null).myStr)
              (sortData, List())
          }

        }
        order match {
          case "desc" => ascSortData.reverse ::: blankDatas
          case "asc" | "" => ascSortData
        }
      }

      def execFilter(sortDatas: List[ujson.Obj]) = {

        def loop(acc: List[ujson.Obj], searchs: List[(String, SearchData)]): List[ujson.Obj] = {
          searchs match {
            case Nil => acc
            case y :: ys =>
              val curFilterData = acc.filter { row =>
                val field = y._1
                val searchData = y._2
                val column = fieldColumnMap(field)
                searchData match {
                  case DateSearchData(startDate, endDate) =>
                    val dbDate = row(field)
                    val startB = if (startDate.nonBlank) {
                      Tool.timeBefore(startDate, dbDate.str)
                    } else true
                    val endB = if (endDate.nonBlank) {
                      Tool.timeAfter(endDate, dbDate.str)
                    } else true
                    startB && endB
                  case TextSearchData(searchType, text) =>
                    val vs = text.split(";")
                    val queryTarget = column.comVF(row.obj.getOrElse(field, ujson.Null), row)
                    searchType match {
                      case "equals" =>
                        vs.exists(v => row.myStr(field) == v)
                      case "like" =>
                        vs.exists(v => queryTarget.contains(v))
                    }
                  case NumericSearchData(min, max, kind) =>
                    val v = column.comVF(row(field), row)
                    v.isDouble && {
                      val dbDouble = v.toDouble
                      val minB = if (min.isDouble) {
                        dbDouble >= min.toDouble
                      } else true
                      val maxB = if (max.isDouble) {
                        dbDouble <= max.toDouble
                      } else true
                      minB && maxB
                    }
                  case CheckboxSearchData(values) =>
                    values.exists(x => row(field).str.split(";").contains(x))
                  case OnlySortSearchData => true
                }
              }
              loop(curFilterData, ys)
          }
        }

        loop(sortDatas, searchMap.toList)
      }

      def allSearchFilter(datas: List[ujson.Obj], titles: List[String]) = {
        val titleFieldMap = fieldColumnMap
          .map { case (field, column) =>
            (column.title, field)
          }
          .filterNot { case (title, field) =>
            List("state", "operate").contains(field)
          }
        datas.filter { row =>
          titleFieldMap.exists { case (title, field) =>
            fieldColumnMap(field).comVF(row.getOrElseNull(field), row).contains(allSearch)
          }
        }
      }

      def selectAll(curIds: List[String])(e: ReactEventFromInput) = {
        val checked = e.target.checked
        if (checked) {
          props.selectedIdsChange(curIds)
        } else {
          props.selectedIdsChange(List.empty)
        }
      }

      def checkboxChange(curId: String)(e: ReactEventFromInput) = {
        val checked = e.target.checked
        if (checked) {
          props.selectedIdsChange(curId :: props.selectedIds)
        } else {
          val newCheckedIs = props.selectedIds.removedV(curId)
          props.selectedIdsChange(newCheckedIs)
        }
      }

    }

    val datas = if (isServer) {
      serverDatas
    } else {
      val sortDatas = FTool.sortExec
      val filterData = FTool.execFilter(sortDatas)
      FTool.allSearchFilter(filterData, props.allSearchTitles)
    }

    val totalRows = if (isServer) {
      serverTotalRows
    } else datas.size
    val pageSizeNum = if (pageSize.isDouble) pageSize.toDouble.toInt else totalRows
    val pageTo = scala.math.min(pageNumber * pageSizeNum, totalRows)
    val pageFrom = (pageNumber - 1) * pageSizeNum + 1
    val curPageDatas = if (isServer) {
      datas
    } else datas.slice(pageFrom - 1, pageTo)

    useEffect(
      IO {
        $(document).myOnSync(
          "click",
          (y: Element, e: Event) => {
            val trueY = e.target.asInstanceOf[Element]
            val b = $(trueY).hasClass("select2-selection__choice__remove") ||
              $(trueY).hasClass("select2-search__field") ||
              $(trueY).hasClass("filterContent") ||
              $(trueY).parents().hasClass("filterContent")
            if (!b) {
              setMenuShow(false)
            } else SyncIO {}
          }
        )
        ()
      },
      List()
    )

    useEffect(
      IO {
        FTool.refreshServerDatas
        ()
      },
      List(
        pageSize,
        pageNumber,
        sortField,
        order,
        searchMap
      )
    )

    useEffect(
      {
        props.selectedIdsChange(List.empty)
      },
      List(pageSize, pageNumber, sortField, order, searchMap, props.datas)
    )

    useEffect(
      {
        var delayFn = null: SetTimeoutHandle
        IO {
          delayFn = js.timers.setTimeout(500) {
            FTool.refreshServerDatas
          }
        }.syncStep.void.map { x =>
          IO {
            js.timers.clearTimeout(delayFn)
          }
        }
      },
      List(allSearch)
    )

    useEffect(
      {
        if (hasFillDefaultV) {
          props.columns.headOption
            .map { column =>
              FTool.confirmFilter(column.field)
            }
            .getOrElse(SyncIO {}) >>
            setHasFillDefaultV(false)
        } else SyncIO {}
      },
      List(hasFillDefaultV)
    )

    useEffect(
      {
        val initTmpSearchMap = props.columns.map { x =>
          val searchData = (x.searchKind match {
            case DateSearch => DateSearchData(startDate = "", endDate = "")
            case TextSearch =>
              if (x.defaultValue.nonBlank) {
                TextSearchData(searchType = "equals", text = x.defaultValue)
              } else TextSearchData(searchType = "like", text = "")
            case NumericSearch => NumericSearchData(min = "", max = "", kind = "int")
            case DoubleSearch => NumericSearchData(min = "", max = "", kind = "double")
            case CheckboxSearch =>
              val curVs = if (x.defaultValue.isBlank) List.empty[String] else List(x.defaultValue)
              CheckboxSearchData(values = curVs)
            case OnlySortSearch => OnlySortSearchData
          }).asInstanceOf[SearchData]
          (x.field, searchData)
        }.toSeqMap
        val initColumnMap = if (props.selectColumnEnabled) {
          if (props.showColumnNames.nonEmpty) {
            props.columns.map { column =>
              val columnShow =
                props.showColumnNames.contains(column.title) || List("state", "operate").contains(column.field)
              (column.field, columnShow)
            }.toSeqMap
          } else {
            props.columns.zipWithIndex
              .filterNot { case (column, curI) =>
                column.field == "operate"
              }
              .map { case (column, curI) =>
                (column.field, curI < props.showColumnNum)
              }
              .toSeqMap
          }
        } else {
          props.columns.zipWithIndex.map { case (column, curI) =>
            (column.field, true)
          }.toSeqMap
        }
        setTmpSearchMap(initTmpSearchMap) >>
          setHasFillDefaultV(true) >>
          setColumnShowMap(initColumnMap)
      },
      List(props.columns)
    )

    useEffect(
      {
        setAllSearch(props.allSearch)
      },
      List(
        props.allSearch
      )
    )

    val minHeightV = if (props.columns.forall(!_.operable)) {
      0
    } else 350

    def paginationE = {
      if (props.pagination) {
        PaginationCp
          .Props(
            totalRows = totalRows,
            pageFrom = pageFrom,
            pageTo = pageTo,
            pageList = props.pageList,
            pageSize = pageSize,
            pageSizeChange = FTool.pageSizeChange,
            pageNumber = pageNumber,
            pageNumberChange = FTool.pageNumberChange
          )
          .render
      } else EmptyVdom
    }

    def sortConditionE = {
      SortConditionCp
        .Props(
          fieldColumnMap = fieldColumnMap,
          sortField = sortField,
          order = order,
          removeSort = FTool.removeSort
        )
        .render
    }

    def exportE = {
      if (props.showExport) {
        div(
          className := "columns columns-right btn-group pull-right",
          div(
            className := "export btn-group",
            button(
              className := "btn btn-default dropdown-toggle",
              ariaLabel := "Export",
              dataToggle := "dropdown",
              title := "Export data",
              i(className := "glyphicon glyphicon-export icon-share"),
              " "
            ),
            ul(
              className := "dropdown-menu",
              role := "menu",
              props.exportData.toVdomArray { x =>
                val showColumnNames = columnShowMap.filter(_._2).keyList
                li(
                  key := x.text,
                  role := "menuitem",
                  x.href.toOption match {
                    case Some(hrefStr) => a(href := hrefStr, x.text)
                    case None =>
                      val clsStr = if (x.disabled) "disabled" else ""
                      a(className := s"${clsStr}", onClick --> x.onClick.get(datas, showColumnNames), x.text)
                  }
                )
              }
            )
          )
        )
      } else {
        EmptyVdom
      }
    }

    def searchE = {
      if (props.search) {
        div(
          className := "pull-right search input-group",
          PureInputCp
            .Props(
              selfClassName = "fsearch-input",
              placeHolder = "Search",
              name = "search",
              value = allSearch,
              onChange = setAllSearch.toSyncIO
            )
            .render
        )
      } else EmptyVdom
    }

    FormProvider(
      control = useForm.control,
      formState = useForm.formState,
      setValue = useForm.setValue _,
      trigger = useForm.trigger _
    )(
      div(
        if (props.selectColumnEnabled) {
          SelectColumnCp
            .Props(
              columnShowMap = columnShowMap,
              fieldColumnMap = fieldColumnMap,
              columnShowMapChange = FTool.columnShowMapChange,
              columnGroupMap = props.columnGroupMap,
              fixedColumns = props.extraFixedColumns ::: List("id", "state", "operate")
            )
            .render
        } else EmptyVdom,
        FilterConditionCp
          .Props(
            searchMap = searchMap,
            fieldColumnMap = fieldColumnMap,
            removeCondition = FTool.removeCondition
          )
          .render,
        sortConditionE,
        div(
          className := "table-responsive",
          div(
            className := "bootstrap-table bootstrap3",
            div(
              className := "fixed-table-toolbar",
              exportE,
              searchE
            ),
            div(
              paddingBottom := 0.px,
              overflowY.auto,
              minHeight := minHeightV.px,
              className := "fixed-table-container",
              div(
                overflow.visible,
                className := "fixed-table-body",
                table(
                  wordWrap.breakWord,
                  id := "myTable",
                  className := "display table table-hover table-bordered",
                  thead(
                    tr(
                      props.columns.zipWithIndex
                        .filter { case (column, curI) =>
                          columnShowMap.getOrElse(column.field, true)
                        }
                        .toVdomArray { case (column, curI) =>
                          if (column.checkbox) {
                            val curIds = curPageDatas.map { curData =>
                              curData.value.getOrElse(props.idField, ujson.Null).myStr
                            }
                            val selectedAll = curIds.toSet == props.selectedIds.toSet
                            th(
                              width := 36.px,
                              key := s"${column.field}",
                              className := "bs-checkbox",
                              div(
                                className := "th-inner",
                                label(
                                  input(
                                    name := "btSelectAll",
                                    `type` := "checkbox",
                                    onChange ==> FTool.selectAll(curIds),
                                    checked := selectedAll
                                  ),
                                  span()
                                )
                              ),
                              div(className := "fht-cell")
                            )
                          } else {
                            th(
                              className := "",
                              key := s"${column.field}",
                              div(
                                overflow.visible,
                                className := "th-inner myJustify",
                                column.titleE.getOrElse(span(column.title)),
                                FilterButtonCp
                                  .Props(
                                    column = column,
                                    sortField = sortField,
                                    sortFieldChange = { (v: String) => setSortField(v) },
                                    order = order,
                                    orderChange = { (v: String) => setOrder(v) },
                                    menuShow = menuShow,
                                    menuShowChange = { (v: Boolean) => setMenuShow(v) },
                                    menuField = menuField,
                                    menuFieldChange = { (v: String) => setMenuField(v) },
                                    tmpSearchMap = tmpSearchMap,
                                    confirmFilter = FTool.confirmFilter,
                                    tmpSearchMapChange = FTool.tmpSearchMapChange
                                  )
                                  .render
                              ),
                              div(className := "fht-cell")
                            )
                          }
                        }
                    )
                  ),
                  tbody(
                    if (datas.isEmpty) {
                      tr(className := "no-records-found", td(colSpan := props.columns.size, t("noMatches").toString))
                    } else {
                      curPageDatas.zipWithIndex.toVdomArray { case (curData, curI) =>
                        val any = curData.value.getOrElse(props.idField, ujson.Null)
                        val curId = any.myStr
                        val selectedCls = if (props.selectedIds.contains(curId)) {
                          "selected"
                        } else ""
                        val checkedV = props.selectedIds.contains(curId)
                        tr(
                          key := s"${curId}",
                          className := s"${selectedCls}",
                          props.columns
                            .filter { column =>
                              columnShowMap.getOrElse(column.field, true)
                            }
                            .toVdomArray { column =>
                              if (column.checkbox) {
                                td(
                                  width := 36.px,
                                  key := s"${column.field}",
                                  className := "bs-checkbox",
                                  div(
                                    className := "th-inner",
                                    label(
                                      input(
                                        name := "btSelectItem",
                                        `type` := "checkbox",
                                        onChange ==> FTool.checkboxChange(curId),
                                        checked := checkedV
                                      ),
                                      span()
                                    )
                                  )
                                )
                              } else {
                                val any = curData.value.getOrElse(column.field, ujson.Null)
                                column.formatter.toOption match {
                                  case Some(f) =>
                                    val ele = f(any, curData)
                                    td(key := s"${column.field}", ele)
                                  case None =>
                                    td(
                                      key := s"${column.field}",
                                      curData(column.field).str
                                    )
                                }
                              }
                            }
                        )
                      }
                    }
                  )
                )
              )
            ),
            paginationE
          )
        )
      )
    )

  }

}
